package com.sencorsta.ids.core.tcp.http;


import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.sencorsta.ids.core.configure.GlobalConfigure;
import com.sencorsta.ids.core.configure.SysConfig;
import com.sencorsta.ids.core.log.Out;
import com.sencorsta.utils.string.StringUtil;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.channel.ChannelHandler.Sharable;
import io.netty.handler.codec.http.*;
import io.netty.handler.codec.http.multipart.Attribute;
import io.netty.handler.codec.http.multipart.FileUpload;
import io.netty.handler.codec.http.multipart.HttpPostRequestDecoder;
import io.netty.handler.codec.http.multipart.InterfaceHttpData;
import io.netty.handler.codec.rtsp.RtspHeaderValues;
import io.netty.handler.ssl.SslHandler;
import io.netty.handler.stream.ChunkedFile;
import io.netty.handler.stream.ChunkedNioFile;

import javax.activation.MimetypesFileTypeMap;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.net.InetSocketAddress;
import java.net.URI;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.util.*;
import java.util.Map.Entry;

import static io.netty.handler.codec.http.HttpHeaderNames.CONNECTION;
import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_TYPE;
import static io.netty.handler.codec.http.HttpResponseStatus.*;
import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1;


/**
 * 　　* @description: Http服务器处理程序
 * 　　* @author TAO
 * 　　* @date 2019/6/12 17:12
 */
@Sharable
public class HttpServerHandler extends SimpleChannelInboundHandler<FullHttpRequest> {

    private HashMap<String, HttpHandler> handlers = new HashMap<>();

    public HttpServerHandler() {

    }

    /**
     * 解析请求参数
     *
     * @return 包含所有请求参数的键值对, 如果没有参数, 则返回空Map
     * @throws
     * @throws IOException
     */
    public static JSONObject parse(FullHttpRequest req) throws Exception {
        HttpMethod method = req.method();
        JSONObject parmMap = new JSONObject();

        if (HttpMethod.POST == method) {
            // 是POST请求
            String type = req.headers().get("Content-Type");
            Out.trace("Content-Type:", type);
            if (type != null && type.indexOf("json") >= 0) {
                byte[] bytes = new byte[req.content().readableBytes()];
                req.content().getBytes(0, bytes);
                String json = new String(bytes, GlobalConfigure.UTF_8);
                parmMap = JSON.parseObject(json);
//				for (Map.Entry<String, Object> entry : jo.entrySet()) {
//					parmMap.put(entry.getKey(), entry.getValue() == null ? null
//							: String.valueOf(entry.getValue()));
//				}
            } else if (type != null && type.indexOf("text/xml") >= 0) {
                parmMap.put("xmlString", req.content().toString(GlobalConfigure.UTF_8));
            } else {
                HttpPostRequestDecoder decoder = new HttpPostRequestDecoder(req);
                decoder.offer(req);
                List<InterfaceHttpData> parmList = decoder.getBodyHttpDatas();
                for (InterfaceHttpData parm : parmList) {
                    Out.trace("HttpDataType：", parm.getHttpDataType());
                    if (parm.getHttpDataType().equals(InterfaceHttpData.HttpDataType.Attribute)) {
                        Attribute data = (Attribute) parm;
                        parmMap.put(data.getName(), data.getValue());
                    } else if (parm.getHttpDataType().equals(InterfaceHttpData.HttpDataType.FileUpload)) {
                        final FileUpload fileUpload = (FileUpload) parm;
                        byte[] file = fileUpload.get();
                        JSONObject fileJson = new JSONObject();
                        fileJson.put("name", fileUpload.getFilename());
                        fileJson.put("data", file);
                        parmMap.put(fileUpload.getName(), fileJson);
//                        final File file = new File(FILE_UPLOAD + fileUpload.getFilename());
//                        Out.info("upload file:", file);
//                        try
//                        {
//                            FileChannel inputChannel = new FileInputStream(fileUpload.getFile()).getChannel();
//                            FileChannel outputChannel = new FileOutputStream(file).getChannel();
//                            outputChannel.transferFrom(inputChannel, 0, inputChannel.size());
//                        }catch (Exception e){
//                            e.printStackTrace();
//                        }

                    }
                }
                //req.release();
//                try {
//                    req.release();
//                    decoder.destroy();
//                }catch (Exception e){
//                    Out.warn("释放资源失败！！！"+e.getMessage());
//                    e.printStackTrace();
//                }
            }

            if (req.uri().indexOf("?") >= 0) {
                QueryStringDecoder query = new QueryStringDecoder(req.uri());
                Set<Entry<String, List<String>>> set = query.parameters().entrySet();
                for (Entry<String, List<String>> entry : set) {
                    parmMap.put(entry.getKey(),
                            entry.getValue().size() == 1 ? entry.getValue().get(0) : entry.getValue());
                }
            }
        } else if (HttpMethod.GET == method) {
            // 是GET请求
            QueryStringDecoder decoder = new QueryStringDecoder(req.uri());
            Set<Entry<String, List<String>>> set = decoder.parameters().entrySet();
            for (Entry<String, List<String>> entry : set) {
                parmMap.put(entry.getKey(),
                        entry.getValue().size() == 1 ? entry.getValue().get(0) : entry.getValue());
            }
        } else if (HttpMethod.OPTIONS == method) {
            return null;
        } else {
            Out.debug("method:", method);
            Out.debug(JSON.toJSONString(req));
            // 不支持其它方法
            // 这是个自定义的异常, 可删掉这一行
            throw new Exception("undefine method : " + method);
        }

        return parmMap;
    }

    /**
     * 响应Http请求
     *
     * @param ctx
     * @param req
     * @param msg
     */
    public static void response(ChannelHandlerContext ctx, FullHttpRequest req, String msg, String ContentType, String FileName) {
        if (StringUtil.isNotEmpty(ContentType)&&(ContentType.equals(MOVED_PERMANENTLY.reasonPhrase())||ContentType.equals(FOUND.reasonPhrase()))){
            FullHttpResponse response;
            if (ContentType.equals(MOVED_PERMANENTLY.reasonPhrase())) {
                response = new DefaultFullHttpResponse(HTTP_1_1, MOVED_PERMANENTLY, Unpooled.wrappedBuffer("".getBytes()));
            }else if (ContentType.equals(FOUND.reasonPhrase())){
                response = new DefaultFullHttpResponse(HTTP_1_1, FOUND, Unpooled.wrappedBuffer("".getBytes()));
            }else {
                response=null;
            }
            Out.warn(msg);
            response.headers().set(HttpHeaderNames.LOCATION, msg);
            ctx.write(response);
            ctx.flush();
            return;
        }
        response(ctx, req, msg, OK, ContentType, FileName);
    }

    /**
     * 响应Http请求
     *
     * @param ctx
     * @param req
     * @param
     */
    public static void response(ChannelHandlerContext ctx, FullHttpRequest req, String msg, HttpResponseStatus
            status, String ContentType, String FileName) {
        Out.debug(req.uri(), " response : ", msg);
        response(ctx, req, msg.getBytes(Charset.forName("UTF-8")), OK, ContentType, FileName);
    }

    public static void response(ChannelHandlerContext ctx, FullHttpRequest req, byte[] msg, HttpResponseStatus
            status, String ContentType, String FileName) {
        try {
            if (StringUtil.isNotEmpty(ContentType)&&(ContentType.equals(HttpContentTypes.APPLICATION_OCTET_STREAM) || ContentType.equals(HttpContentTypes.Auto))) {
                //文件下传
                File file = new File(new String(msg, Charset.forName("UTF-8")));
                RandomAccessFile randomAccessFile = new RandomAccessFile(file, "r");
                long fileLength = randomAccessFile.length();
                Out.debug("file:", file.getAbsolutePath());
                Out.debug("fileLength:", fileLength);

                HttpResponse response = new DefaultHttpResponse(HTTP_1_1, OK);
                response.headers().set("Content-Length", fileLength);

                if (ContentType.equals(HttpContentTypes.Auto)) {
                    MimetypesFileTypeMap mimetypesTypeMap = new MimetypesFileTypeMap();
                    response.headers().set(CONTENT_TYPE, mimetypesTypeMap.getContentType(file.getPath()));
                    //response.headers().set("content-disposition", "inline");
                }else {
                    response.headers().set("Content-Type", ContentType);
                    String userAgent = req.headers().get("User-Agent");
                    String encoderFilename = FileName;
                    if (userAgent.toUpperCase().contains("MSIE") || userAgent.contains("Trident/7.0")) {
                        encoderFilename = URLEncoder.encode(FileName, "UTF-8");
                    } else if (userAgent.toUpperCase().contains("MOZILLA") || userAgent.toUpperCase().contains("CHROME")) {
                        encoderFilename = new String(FileName.getBytes(), "ISO-8859-1");
                    } else {
                        encoderFilename = URLEncoder.encode(FileName, "UTF-8");
                    }
                    Out.debug("encoderFilename:", encoderFilename);
                    response.headers().set("content-disposition", "attachment;filename=" + encoderFilename);
                }

                response.headers().set("Access-Control-Allow-Origin", "*");
                response.headers().set("Charset", "UTF-8");
                if (HttpUtil.isKeepAlive(req)) {
                    response.headers().set(CONNECTION, HttpHeaderValues.KEEP_ALIVE);
                }
                ctx.write(response);
                ChannelFuture sendFileFuture;
                //通过Netty的ChunkedFile对象直接将文件写入到发送缓冲区中
                sendFileFuture = ctx.write(new ChunkedFile(randomAccessFile, 0, fileLength, 8192), ctx.newProgressivePromise());
                //为sendFileFuture添加监听器，如果发送完成打印发送完成的日志
                sendFileFuture.addListener(new ChannelProgressiveFutureListener() {
                    @Override
                    public void operationProgressed(ChannelProgressiveFuture future, long progress, long total)
                            throws Exception {
                        if (total < 0) {
                            Out.trace(FileName + " 传输中: " + progress);
                        } else {
                            Out.trace(FileName + " 传输中: " + progress + "/" + total);
                        }
                    }

                    @Override
                    public void operationComplete(ChannelProgressiveFuture future)
                            throws Exception {
                        Out.debug(FileName + " 传输完成！");
                    }


                });

                //如果使用chunked编码，最后需要发送一个编码结束的空消息体，将LastHttpContent.EMPTY_LAST_CONTENT发送到缓冲区中，
                //来标示所有的消息体已经发送完成，同时调用flush方法将发送缓冲区中的消息刷新到SocketChannel中发送
                ChannelFuture lastContentFuture = ctx.
                        writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);
                //如果是非keepAlive的，最后一包消息发送完成后，服务端要主动断开连接
                if (!HttpUtil.isKeepAlive(req)) {
                    lastContentFuture.addListener(ChannelFutureListener.CLOSE);
                }

            } else {
                FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, status, Unpooled.wrappedBuffer(msg));

                if (StringUtil.isEmpty(ContentType)) {
                    response.headers().set("Content-Type", HttpContentTypes.TEXT_PLAIN);
                } else {
                    response.headers().set("Content-Type", ContentType);
                    if (ContentType.equals(HttpContentTypes.APPLICATION_OCTET_STREAM)) {
                        response.headers().set("content-disposition", "attachment;filename=" + FileName);
                    }
                    response.headers().set("Content-Type", ContentType);
                }
                response.headers().set("Content-Length", response.content().readableBytes());
                response.headers().set("Access-Control-Allow-Origin", "*");
                if (HttpUtil.isKeepAlive(req)) {
                    response.headers().set("Connection", RtspHeaderValues.KEEP_ALIVE);
                }
                ctx.write(response);
            }
            ctx.flush();

        } catch (Exception e) {
            e.printStackTrace();
        }

    }

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) throws Exception {
        Out.trace("新的HTTP请求：", request.uri());
        URI uri = new URI(request.uri());

//        if (uri.getPath().equals("/favicon.ico")) {
//            return;
//        }

        String path = uri.getPath();
        HttpHandler handler = handlers.get(path);
        if (handler != null) {
            Out.debug(request.method(), " : ", request.uri());
            JSONObject args = parse(request);
            if (request.method().equals(HttpMethod.POST)) {
                if (handler.getValidate(ctx, request, args) == true) {
                    String res = handler.doPost(ctx, request, args);
                    if (res != null) {
                        response(ctx, request, res, handler.contentType, handler.fileName);
                    }
                } else {
                    response(ctx, request, "403".getBytes(), HttpResponseStatus.FORBIDDEN, null, null);
                }

            } else if (request.method().equals(HttpMethod.GET)) {
                if (handler.getValidate(ctx, request, args) == true) {
                    String res = handler.doGet(ctx, request, args);
                    if (res != null) {
                        response(ctx, request, res, handler.contentType, handler.fileName);
                    }
                } else {
                    response(ctx, request, "403".getBytes(), HttpResponseStatus.FORBIDDEN, null, null);
                }
            } else if (request.method().equals(HttpMethod.OPTIONS)) {
                FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, OK);
                response.headers().set("Access-Control-Allow-Origin", "*");
                response.headers().set("Access-Control-Allow-Credentials", "true");
                response.headers().set("Access-Control-Allow-Methods", "*");
                response.headers().set("Access-Control-Allow-Headers", "Content-Type,Access-Token");
                response.headers().set("Access-Control-Expose-Headers", "*");
                ctx.write(response);
                ctx.flush();
                ctx.close();
            }
        } else {
            //尝试拉起本地文件
            if(SysConfig.getInstance().getBoolean("openStaticWeb",false)){
                Out.trace("没有handler 尝试找本地文件 url：", uri);
                // 设置不支持favicon.ico文件
//                if ("favicon.ico".equals(uri)) {
//                    return
//                }
                // 根据路径地址构建文件
                String basePath=System.getProperty("user.dir")+SysConfig.getInstance().get("pathStaticWeb","/web");
                String pathFile =  basePath+ URLDecoder.decode(uri.getPath(),"UTF-8");
                // 构建404页面
                String path404 = basePath + "/404.html";
                File html = new File(pathFile);

                // 状态为1xx的话，继续请求
                if (HttpUtil.is100ContinueExpected(request)) {
                    FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.CONTINUE);
                    ctx.writeAndFlush(response);
                }
                //如果是目录就加默认index.html
                if (html.isDirectory()){
                    html = new File(pathFile+"/index.html");
                }



                // 当文件不存在的时候，将资源指向NOT_FOUND
                boolean isNotFound=false;
                if (!html.exists()) {
                    html = new File(path404);
                    if (!html.exists()){
                        InetSocketAddress insocket = (InetSocketAddress) ctx.channel().remoteAddress();
                        String clientIP = insocket.getAddress().getHostAddress();
                        Out.warn("没有找到页面:", path404, " : ", request.uri(), " IP:", clientIP);
                        ctx.close();
                        return;
                    }else {
                        isNotFound=true;
                    }
                }

                RandomAccessFile file = new RandomAccessFile(html, "r");
                HttpResponse response = new DefaultHttpResponse(HTTP_1_1, HttpResponseStatus.OK);

                // 文件没有发现设置状态为404
                if (isNotFound) {
                    response.setStatus(HttpResponseStatus.NOT_FOUND);
                }

                // 设置文件格式内容
                if (path.endsWith(".html")){
                    response.headers().set("Content-Type", "text/html; charset=UTF-8");
                }else if(path.endsWith(".js")){
                    response.headers().set("Content-Type", "application/x-javascript");
                }else if(path.endsWith(".css")){
                    response.headers().set("Content-Type", "text/css; charset=UTF-8");
                }
                boolean keepAlive = HttpUtil.isKeepAlive(request);

                if (keepAlive) {
                    response.headers().set("Content-Length", file.length());
                    response.headers().set("Connection", "keep-alive");
                }
                ctx.write(response);
                if (ctx.pipeline().get(SslHandler.class) == null) {
                    ctx.write(new DefaultFileRegion(file.getChannel(), 0, file.length()));
                } else {
                    ctx.write(new ChunkedNioFile(file.getChannel()));
                }
                // 写入文件尾部
                ChannelFuture future = ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);
                if (!keepAlive) {
                    future.addListener(ChannelFutureListener.CLOSE);
                }
                file.close();
                Out.trace("找到本地文件 url：", uri);
            }else {
                InetSocketAddress insocket = (InetSocketAddress) ctx.channel().remoteAddress();
                String clientIP = insocket.getAddress().getHostAddress();
                Out.warn("没有找到处理句柄:", request.method(), " : ", request.uri(), " IP:", clientIP);
                ctx.close();
            }

        }
        //ctx.close();
    }

    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        ctx.flush();
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        Out.trace("http服务器异常:" + ctx.channel().remoteAddress(), cause.getMessage(), cause.toString());
        if (!(cause instanceof IOException)) {
            Out.error("http服务器异常:" + ctx.channel().remoteAddress(), cause.getMessage(), cause.toString());
            cause.printStackTrace();
        } else {
            ctx.close();
        }
    }

    public void addHandler(HttpHandler handler) {
        HttpHandler old = handlers.put(handler.getPath(), handler);
        if (old != null) {
            Out.warn(handler.getPath(), " exists handler : ", old.getClass().getName(),
                    "\n current use handler :", handler.getClass().getName());
        }
    }

    public void echo() {
        for (Entry<String, HttpHandler> entry : handlers.entrySet()) {
            Out.debug("注册HttpHandler " + entry.getKey() + " : " + entry.getValue().getClass().getName());
        }
    }

}
